﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using log4net;

namespace UDPPunchTest
{
    /// <summary>
    /// Demonstration of UDP punching technique using UDP.  The server has a well known external IP address
    /// and the clients can be behind firewalls.
    /// </summary>
    class UDPPunchServer
    {
        private Socket m_udp_server_socket;
        private int m_endpoint_port;
        private Thread m_listen_thread;
        private static readonly ILog log = LogManager.GetLogger(typeof(UDPPunchServer));
        private Dictionary<string, string> m_punch_clients = new Dictionary<string, string>();

        /// <summary>
        /// Configure the punch server to listen to messages for the specified port
        /// </summary>
        /// <param name="port"></param>
        public UDPPunchServer(int port)
        {
            m_endpoint_port = port;
        }

        /// <summary>
        /// Start the punch server and start listening to datagram packets.
        /// </summary>
        public void start()
        {
            lock (this)
            {
                if (m_udp_server_socket == null) 
                {
                    // create the socket and bind to the port
                    m_udp_server_socket = new Socket(AddressFamily.InterNetwork,SocketType.Dgram, ProtocolType.Udp);
                    IPEndPoint ipep = new IPEndPoint(IPAddress.Any, m_endpoint_port);
                    m_udp_server_socket.Bind(ipep);
                }

                if (m_listen_thread == null)
                {
                    // create the server thread and start listening to incoming messages
                    m_listen_thread = new Thread(listenThread);
                    m_listen_thread.Start();
                }
            }
        }

        /// <summary>
        /// The listener thread which waits for incoming packets
        /// </summary>
        private void listenThread()
        {
            System.Net.EndPoint remote_ep = new System.Net.IPEndPoint(IPAddress.Any, 0);
            byte[] data = new byte[64*1024];
            int length = 0;
            log.Info("Starting UDP server listening to port " + m_udp_server_socket.LocalEndPoint.ToString());
            while (true)
            {
                try
                {
                    length = m_udp_server_socket.ReceiveFrom(data, ref remote_ep);
                    if (length > 0)
                    {
                        string recv_string = Encoding.ASCII.GetString(data, 0, length);
                        parsePunchClientData(recv_string,remote_ep);
                    }
                }
                catch (SocketException e)
                {
                    log.Error("Got a socket error",e);
                }
            }
        }

        /// <summary>
        /// Parse the string data received from the PunchClient and process them
        /// as commands.  Currently 2 commands are supported
        /// 1. register -   registers the punch client with the server and the server
        ///                 remembers the punch clients ip address
        /// 2. lookup -     looks up the client by name and sends its ip address to the 
        ///                 punch client.
        /// </summary>
        /// <param name="recv_string"></param>
        private void parsePunchClientData(string recv_string,EndPoint remote_ep)
        {
            PunchMessage pmsg = PunchMessage.parseMessage(recv_string);
            switch (pmsg.m_command)
            {
                case "register":
                    {
                        m_punch_clients[pmsg.m_command_param] = remote_ep.ToString();
                        log.Info("Registered client=" + pmsg.m_command_param + " with ip=" + remote_ep.ToString());
                        sendPunchMessage("client_registered", "true",remote_ep);
                        break;
                    }
                case "lookup":
                    {
                        // sends the ip address of the client to the punch client
                        string value = pmsg.m_command_param;
                        if (m_punch_clients.ContainsKey(value))
                        {
                            string ip = m_punch_clients[value];
                            //log.Info("Sending information of client " + value + " with ip=" + ip + " to client at ip=" + remote_ep.ToString());
                            sendPunchMessage("peer_address", ip, remote_ep);
                        }
                        else
                        {
                            sendPunchMessage(PunchMessage.PEER_ADDRESS_NOT_FOUND, value, remote_ep);
                        }
                        break;
                    }
            }
        }

        private void sendPunchMessage(string command, string param,EndPoint ep)
        {
            PunchMessage pmsg = new PunchMessage(command, param);
            m_udp_server_socket.SendTo(pmsg.serialize(), ep);
        }

        private void sendStringData(string str, EndPoint ep)
        {
            byte[] data = Encoding.ASCII.GetBytes(str);
            m_udp_server_socket.SendTo(data, ep);
        }
    }
}
